#!/usr/bin/env python3
"""
Claude Code UI Frame Renderer — v4 ULTRA
Multi-frame per-scene rendering with progressive animation.
Each scene generates N frames at 30fps with incrementally different content.

Key features:
- Progress bars that GROW from 0→target% across frames
- Token counts that INCREMENT across frames
- Subtask lines that REVEAL one by one
- Code lines that TYPE in progressively
- Diff lines that SLIDE in
- Error flash at specific moment
- Agent lifecycle: running → completing → done
- Real Claude Code status bar details
- Multi-agent with 2-3 workers and layered layout
"""

import json
import os
import math
from PIL import Image, ImageDraw, ImageFont

# === CONFIG ===
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CONFIG_PATH = os.path.join(BASE, "v11_kimi_bugfix", "scenes_v11", "scenes_v11.json")
FRAMES_DIR = os.path.join(BASE, "04_frames_v11")
FPS = 30
W, H = 1080, 1920

os.makedirs(FRAMES_DIR, exist_ok=True)

# === Claude Code Dark Terminal Palette ===
BG_TERMINAL = (18, 18, 22)
BG_INPUT = (22, 22, 28)
BG_HOVER = (30, 30, 38)
BG_CARD = (24, 26, 34)
TEXT_PRIMARY = (220, 222, 228)
TEXT_SECONDARY = (148, 152, 162)
TEXT_DIM = (100, 104, 114)
TEXT_ORANGE = (255, 160, 50)
TEXT_GREEN = (80, 220, 120)
TEXT_RED = (240, 80, 80)
TEXT_BLUE = (80, 160, 240)
TEXT_YELLOW = (240, 200, 60)
TEXT_PURPLE = (180, 120, 240)
BORDER_COLOR = (42, 42, 48)
DIVIDER_COLOR = (38, 38, 44)
PROGRESS_BG = (40, 40, 50)
CURSOR_COLOR = (220, 222, 228)

# === Fonts (even bigger for V4 mobile) ===
FONT_REG = "C:/Windows/Fonts/msyh.ttc"
FONT_BOLD = "C:/Windows/Fonts/msyhbd.ttc"
FONT_MONO = "C:/Windows/Fonts/simsun.ttc"


def get_font(size, bold=False, mono=False):
    paths = []
    if mono:
        paths.append(FONT_MONO)
    if bold:
        paths.append(FONT_BOLD)
    paths.append(FONT_REG)
    for p in paths:
        try:
            return ImageFont.truetype(p, size)
        except:
            continue
    return ImageFont.load_default()


def create_base_image():
    return Image.new("RGB", (W, H), BG_TERMINAL)


def draw_text_ellipsis(draw, text, x, y, font, color, max_w=None):
    if max_w and font.getlength(text) > max_w:
        while font.getlength(text + "…") > max_w and len(text) > 3:
            text = text[:-1]
        text += "…"
    draw.text((x, y), text, fill=color, font=font)


def lerp(a, b, t):
    """Linearly interpolate between a and b by t (clamped 0-1)."""
    t = max(0, min(1, t))
    if isinstance(a, (int, float)) and isinstance(b, (int, float)):
        return a + (b - a) * t
    return a


def draw_progress_bar(draw, x, y, w, h, percent, color=TEXT_GREEN, bg_color=PROGRESS_BG, show_pct=True, pct_font_size=16):
    """Draw a progress bar at given percent (0-100)."""
    draw.rounded_rectangle([x, y, x + w, y + h], radius=h // 2, fill=bg_color)
    fill_w = min(int(w * percent / 100), w)
    if fill_w > 0:
        draw.rounded_rectangle([x, y, x + fill_w, y + h], radius=h // 2, fill=color)
    if show_pct:
        pf = get_font(pct_font_size, bold=True)
        draw.text((x + w + 12, y - 1), "%d%%" % int(percent), fill=color, font=pf)


def format_time(seconds):
    """Format seconds as MM:SS."""
    m = int(seconds) // 60
    s = int(seconds) % 60
    return "%02d:%02d" % (m, s)


def format_tokens(n):
    """Format token count with K suffix."""
    if n >= 1000:
        return "%.1fk" % (n / 1000)
    return str(n)


# ================================================================
# RENDER: idle_terminal_state
# ================================================================
def render_idle_terminal_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)
    frame = int(progress * 2.5 * FPS)  # absolute frame index

    # Mode indicator — top left green dot + "CODE"
    draw.text((40, 30), "●", fill=TEXT_GREEN, font=get_font(16))
    draw.text((62, 28), "CODE", fill=TEXT_GREEN, font=get_font(16, bold=True))

    # Main prompt — big, clear
    placeholder = payload.get("prompt_placeholder", "Type a prompt or / for commands")
    prompt_font = get_font(28)
    draw.text((40, 80), placeholder, fill=TEXT_DIM, font=prompt_font)

    # Cursor blink — visible every 15 frames (0.5s)
    if frame % 30 < 15:
        cursor_x = 40 + int(prompt_font.getlength(placeholder)) + 4
        draw.rectangle([cursor_x, 80, cursor_x + 16, 112], fill=CURSOR_COLOR)

    # Divider
    draw.line([(40, 140), (W - 40, 140)], fill=DIVIDER_COLOR, width=2)

    # === Status Bar (real Claude Code style) ===
    sb = payload.get("status_bar", {})
    sb_font = get_font(18)
    sb_bg = BG_INPUT

    # Status bar background
    draw.rectangle([(40, 175), (W - 40, 235)], fill=sb_bg, outline=BORDER_COLOR, width=1)

    # Left: ● main
    draw.text((55, 191), sb.get("left", "● main"), fill=TEXT_GREEN, font=sb_font)

    # Right side items
    right_items = [
        sb.get("mode", "bypass permissions on"),
        sb.get("context", "97% context used")
    ]
    x_pos = W - 60
    for item in reversed(right_items):
        item_w = sb_font.getlength(item)
        x_pos -= item_w + 20
        draw.text((x_pos, 191), item, fill=TEXT_GREEN if "bypass" in item else TEXT_DIM, font=sb_font)

    # Bottom hints
    hint_font = get_font(15)
    draw.text((55, 252), sb.get("hint", "shift+tab to cycle"), fill=TEXT_DIM, font=hint_font)
    draw.text((300, 252), sb.get("manage", "↓ to manage tools"), fill=TEXT_DIM, font=hint_font)
    draw.text((540, 252), sb.get("interrupt", "esc to interrupt"), fill=TEXT_DIM, font=hint_font)
    draw.text((780, 252), "esc to interrupt · / to commands", fill=TEXT_DIM, font=hint_font)

    # Agent waiting status
    if sb.get("agent_hint"):
        draw.text((55, 290), sb["agent_hint"], fill=TEXT_YELLOW, font=get_font(18))

    # === Recent Commands (appear progressively) ===
    commands = payload.get("recent_commands", [])
    cmd_font = get_font(22, mono=True)
    cmd_start_y = 360

    # Only show commands revealed by progress
    total_cmds = len(commands)
    reveal_count = min(total_cmds, int(total_cmds * progress * 1.5) + 1)

    for i in range(reveal_count):
        cmd = commands[i]
        y = cmd_start_y + i * 100

        # Command line
        draw.text((50, y), cmd.get("cmd", ""), fill=TEXT_DIM, font=cmd_font)

        # Result line (dimmed, slightly indented)
        result = cmd.get("result", "")
        rc = cmd.get("result_color", "green")
        cm = {"green": TEXT_GREEN, "yellow": TEXT_YELLOW, "red": TEXT_RED, "dim": TEXT_DIM}
        draw.text((70, y + 40), result, fill=cm.get(rc, TEXT_GREEN), font=cmd_font)

        # Spacer
        if i < reveal_count - 1:
            draw.line([(50, y + 85), (W - 50, y + 85)], fill=DIVIDER_COLOR, width=1)

    # Decorative terminal border
    draw.rectangle([(0, 0), (W, H)], outline=(38, 38, 44), width=1)

    return img


# ================================================================
# RENDER: context_compression_state
# ================================================================
def render_context_compression_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)

    old_t = payload.get("old_tokens", 85000)
    new_t = payload.get("new_tokens", 32000)
    saved = old_t - new_t
    ratio = saved / old_t if old_t > 0 else 0.62

    # Animated values based on progress
    # Phase 1 (0-0.6): countdown old→new
    # Phase 2 (0.6-1.0): show result with bar

    if progress < 0.6:
        sub_progress = progress / 0.6
        current_new = int(lerp(old_t, new_t, sub_progress))
        display_old = old_t
    else:
        display_old = old_t
        current_new = new_t

    # === Badge ===
    draw.rounded_rectangle([40, 60, 320, 115], radius=10, fill=(50, 50, 70), outline=(80, 80, 100), width=2)
    draw.text((180, 86), "⟳ CONTEXT COMPRESSION", fill=TEXT_YELLOW, font=get_font(20, bold=True), anchor="mm")

    # === Main message ===
    draw.text((540, 200), payload.get("message", ""), fill=TEXT_PRIMARY, font=get_font(30, bold=True), anchor="mm")

    # === Token display ===
    token_font = get_font(56, bold=True)
    label_font = get_font(18)

    # Old tokens
    old_str = "%d" % display_old
    draw.text((250, 330), old_str, fill=TEXT_DIM, font=token_font, anchor="mm")
    draw.text((250, 400), "before", fill=TEXT_DIM, font=label_font, anchor="mm")

    # Arrow
    draw.text((540, 360), "→", fill=TEXT_YELLOW, font=get_font(42, bold=True), anchor="mm")

    # New tokens (animated)
    new_str = "%d" % current_new
    if progress >= 0.6:
        draw.text((830, 330), new_str, fill=TEXT_GREEN, font=token_font, anchor="mm")
        draw.text((830, 400), "after", fill=TEXT_DIM, font=label_font, anchor="mm")
    else:
        # Counting down — show digits flickering
        draw.text((830, 330), new_str, fill=TEXT_YELLOW, font=token_font, anchor="mm")
        draw.text((830, 400), "compressing...", fill=TEXT_DIM, font=label_font, anchor="mm")

    # === Compression bar ===
    bar_x, bar_y = 140, 500
    bar_w, bar_h = 800, 24

    # Background bar
    draw.rounded_rectangle([bar_x, bar_y, bar_x + bar_w, bar_y + bar_h], radius=12, fill=PROGRESS_BG)

    # Filled portion — grows with progress
    if progress < 0.6:
        bar_progress = 0
    else:
        bar_progress = (progress - 0.6) / 0.4 * 100

    fill_w = int(bar_w * bar_progress / 100)
    if fill_w > 0:
        draw.rounded_rectangle([bar_x, bar_y, bar_x + fill_w, bar_y + bar_h], radius=12, fill=TEXT_GREEN)

    # Percentage label
    if progress >= 0.6:
        pct = int(ratio * 100)
        draw.text((540, 550), "%d%% compression" % pct, fill=TEXT_GREEN, font=get_font(22, bold=True), anchor="mm")
        draw.text((540, 590), "%d tokens saved" % saved, fill=TEXT_DIM, font=get_font(18), anchor="mm")

    return img


# ================================================================
# RENDER: multi_agent_launch_state
# ================================================================
def render_multi_agent_launch_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)
    total_frames = int(4.0 * FPS)
    frame_idx = int(progress * total_frames)

    main_task = payload.get("main_task", {})
    subtasks = payload.get("subtasks", [])
    agents = payload.get("agents", [])
    anim = payload.get("animation", {})

    # === MAIN TASK HEADER ===
    draw.text((40, 40), "■", fill=TEXT_ORANGE, font=get_font(26))
    draw_text_ellipsis(draw, main_task.get("title", ""), 72, 42, get_font(24, bold=True), TEXT_ORANGE, max_w=960)

    # Task meta
    meta_font = get_font(18)
    elapsed_str = format_time(main_task.get("elapsed_seconds", 2) * progress)
    tokens_val = int(main_task.get("tokens", 500) * progress)
    draw.text((72, 78), "(%s · ↓ %s tokens)" % (elapsed_str, format_tokens(tokens_val)), fill=TEXT_DIM, font=meta_font)

    # Divider
    draw.line([(40, 120), (W - 40, 120)], fill=DIVIDER_COLOR, width=2)

    # === SUBTASK TREE — progressive reveal ===
    sub_font = get_font(20)
    sub_y = 150
    total_sub = len(subtasks)

    for i, st in enumerate(subtasks):
        title = st.get("title", "")
        indent = st.get("indent", 1)
        status = st.get("status", "pending")

        # Should this subtask be visible?
        reveal_at = (i + 1) / total_sub * 0.75
        if progress < reveal_at:
            continue

        # Determine status based on progress
        if status == "running" and progress > 0.4:
            status = "done"
        if status == "pending" and progress > 0.25 and i <= progress * total_sub:
            status = "running"

        if status == "running":
            sym = "■"
            sc = TEXT_ORANGE
            tc = TEXT_PRIMARY
            # Orange left bar
            draw.rectangle([(38, sub_y), (42, sub_y + 30)], fill=TEXT_ORANGE)
        elif status == "done":
            sym = "✓"
            sc = TEXT_GREEN
            tc = TEXT_SECONDARY
        else:
            sym = "□"
            sc = TEXT_DIM
            tc = TEXT_DIM

        draw.text((50 + indent * 18, sub_y), sym, fill=sc, font=sub_font)
        draw_text_ellipsis(draw, title, 80 + indent * 18, sub_y, sub_font, tc, max_w=880 - indent * 18)
        sub_y += 42

    # === AGENTS SECTION ===
    agent_section_y = sub_y + 20
    draw.text((40, agent_section_y), "THINKING", fill=TEXT_DIM, font=get_font(16, bold=True))

    # Thought indicator
    thought_y = agent_section_y + 35
    thought_dots = "." * (frame_idx // 10 % 4)
    draw.text((55, thought_y), "Thought for %ds%s" % (int(progress * 3), thought_dots),
              fill=TEXT_YELLOW, font=get_font(18))

    # Tool call card area — show tool being considered
    tool_y = thought_y + 40
    if progress < 0.3:
        # Thinking phase - no agents yet
        pass_msg = "Planning approach for multi-agent pipeline..."
        draw.text((55, tool_y), pass_msg, fill=TEXT_DIM, font=get_font(18))
    elif progress < 0.5:
        # Consider agent spawning
        draw.rounded_rectangle([(40, tool_y), (W - 40, tool_y + 80)], radius=8,
                               fill=BG_CARD, outline=BORDER_COLOR, width=1)
        draw.text((60, tool_y + 15), "▶ Spawning agents...", fill=TEXT_YELLOW, font=get_font(20))
        draw.text((60, tool_y + 50), "allocating tasks to workers",
                  fill=TEXT_DIM, font=get_font(16))

    # === AGENT LIST ===
    agent_y = tool_y + 110
    draw.text((40, agent_y), "AGENTS", fill=TEXT_DIM, font=get_font(14, bold=True))
    agent_y += 30

    for i, agent in enumerate(agents):
        launch_delay = agent.get("launch_delay", 0)
        agent_progress = agent.get("progress", 50)
        end_progress = agent.get("final_tokens", 30000)

        # Agent becomes visible after launch_delay
        agent_reveal = max(0, (progress - launch_delay / 4.0) / (1 - launch_delay / 4.0))
        if agent_reveal <= 0:
            agent_y += 110
            continue

        name = agent.get("name", "")
        role = agent.get("role", "worker")
        current_progress = int(agent_progress * agent_reveal)
        current_tokens = int(end_progress * agent_reveal)
        current_elapsed = int(agent.get("elapsed_seconds", 20) * agent_reveal)
        agent_status = agent.get("status", "running")

        if agent_reveal > 0.9:
            agent_status = "running"
        elif agent_reveal < 0.1:
            agent_status = "spawning"

        # Agent row background
        draw.rounded_rectangle([(40, agent_y), (W - 40, agent_y + 90)], radius=8,
                               fill=(22, 24, 30) if agent_status == "running" else (20, 20, 26),
                               outline=(50, 50, 58) if agent_status == "running" else BORDER_COLOR,
                               width=1)

        # Agent dot
        if role == "main":
            draw.ellipse([(55, agent_y + 12), (75, agent_y + 32)], fill=(200, 200, 210))
            draw.text((90, agent_y + 8), name, fill=TEXT_PRIMARY, font=get_font(22, bold=True))
        else:
            if agent_status == "running":
                draw.ellipse([(55, agent_y + 12), (75, agent_y + 32)], outline=HOLLOW_CIRCLE, width=3)
                draw.ellipse([(59, agent_y + 16), (71, agent_y + 28)], fill=TEXT_ORANGE)
            else:
                draw.ellipse([(55, agent_y + 12), (75, agent_y + 32)], outline=HOLLOW_CIRCLE, width=2)
            draw.text((90, agent_y + 8), name, fill=TEXT_SECONDARY, font=get_font(20))

        # Task
        task = agent.get("task", "")
        if task and agent_reveal > 0.2:
            draw_text_ellipsis(draw, task, 90, agent_y + 38, get_font(16), TEXT_ORANGE if agent_status == "running" else TEXT_DIM, max_w=700)

        # Progress bar (for workers)
        if role != "main" and agent_reveal > 0.1:
            draw_progress_bar(draw, 90, agent_y + 60, 450, 12, current_progress,
                            TEXT_GREEN if current_progress > 0 else TEXT_DIM)

        # Meta right-aligned
        meta_parts = []
        meta_parts.append(format_time(current_elapsed))
        meta_parts.append("↓ %s" % format_tokens(current_tokens))
        m_str = " · ".join(meta_parts)
        m_font = get_font(15)
        m_w = m_font.getlength(m_str)
        draw.text((W - 60 - m_w, agent_y + 10), m_str, fill=TEXT_DIM, font=m_font)

        agent_y += 105

    # === WAITING STATUS ===
    if progress > 0.3 and progress < 0.9:
        wait_text = payload.get("waiting_status", "Waiting for background agents...")
        draw.text((55, agent_y + 15), wait_text, fill=TEXT_YELLOW, font=get_font(16))

    # === BOTTOM STATUS BAR ===
    draw.rectangle([(40, H - 60), (W - 40, H - 10)], fill=BG_INPUT, outline=BORDER_COLOR, width=1)
    draw.text((55, H - 45), "● main", fill=TEXT_GREEN, font=get_font(14))
    ctx_pct = int(60 + 30 * progress)
    draw.text((W - 200, H - 45), "%d%% context used" % ctx_pct, fill=TEXT_DIM, font=get_font(14))

    return img


# ================================================================
# RENDER: multi_agent_running_state
# ================================================================
def render_multi_agent_running_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)
    frame_idx = int(progress * 5.0 * FPS)

    agents = payload.get("agents", [])
    subtasks = payload.get("subtasks", [])
    tool_calls = payload.get("tool_calls", [])

    # === AGENTS SECTION — full screen focus ===
    draw.text((40, 30), "AGENTS RUNNING", fill=TEXT_DIM, font=get_font(16, bold=True))

    # Draw agent cards
    card_y = 70
    for i, agent in enumerate(agents):
        name = agent.get("name", "")
        role = agent.get("role", "")
        status = agent.get("status", "running")
        end_progress = agent.get("progress_end", 80)

        # Progress grows from animation_start to current
        current_progress = int(end_progress * progress)

        # Elapsed counter animation
        elapsed_val = int(agent.get("elapsed_seconds", 60) * progress)
        elapsed_str = format_time(elapsed_val)

        # Token counter animation
        tokens_val = int(agent.get("tokens", 18000) * progress)

        # Agent card
        card_h = 100
        draw.rounded_rectangle([(40, card_y), (W - 40, card_y + card_h)], radius=10,
                               fill=(22, 24, 32), outline=(50, 50, 60), width=1)

        # Dot
        if role == "main":
            draw.ellipse([(58, card_y + 14), (78, card_y + 34)], fill=(200, 200, 210))
            draw.text((96, card_y + 12), name, fill=TEXT_PRIMARY, font=get_font(22, bold=True))
        else:
            draw.ellipse([(58, card_y + 14), (78, card_y + 34)], outline=HOLLOW_CIRCLE, width=3)
            draw.ellipse([(62, card_y + 18), (74, card_y + 30)], fill=TEXT_ORANGE if status == "running" else TEXT_GREEN)
            draw.text((96, card_y + 12), name, fill=TEXT_SECONDARY, font=get_font(20))

        # Task
        task = agent.get("task", "")
        if task:
            draw_text_ellipsis(draw, task, 96, card_y + 42, get_font(16), TEXT_ORANGE if status == "running" else TEXT_SECONDARY, max_w=680)

        # Progress bar
        if role != "main":
            draw_progress_bar(draw, 96, card_y + 66, 500, 14, current_progress, TEXT_GREEN)

        # Meta right
        meta = "%s · ↓ %s" % (elapsed_str, format_tokens(tokens_val))
        mf = get_font(15)
        mw = mf.getlength(meta)
        draw.text((W - 60 - mw, card_y + 14), meta, fill=TEXT_DIM, font=mf)

        # Status badge right side
        if current_progress >= 100:
            draw.text((W - 140, card_y + 42), "✓ done", fill=TEXT_GREEN, font=get_font(18, bold=True))
        elif current_progress > 50:
            draw.text((W - 170, card_y + 42), "%d%%" % current_progress, fill=TEXT_ORANGE, font=get_font(18, bold=True))
        else:
            draw.text((W - 170, card_y + 42), "%d%%" % current_progress, fill=TEXT_YELLOW, font=get_font(18, bold=True))

        card_y += 115

    # === SUBTASK PROGRESS ===
    sub_y = card_y + 15
    draw.text((40, sub_y), "SUBTASKS", fill=TEXT_DIM, font=get_font(14, bold=True))
    sub_y += 30

    sub_font = get_font(18)
    for i, st in enumerate(subtasks):
        title = st.get("title", "")
        indent = st.get("indent", 1)
        status = st.get("status", "pending")

        # Progressive completion based on overall progress
        show = (i + 1) / len(subtasks)
        if status == "pending" and progress > show * 0.3:
            status = "running"
        if status == "running" and progress > show * 0.7:
            status = "done"

        sym = {"running": "■", "done": "✓", "pending": "□"}.get(status, "□")
        sc = {"running": TEXT_ORANGE, "done": TEXT_GREEN, "pending": TEXT_DIM}.get(status, TEXT_DIM)
        tc = {"running": TEXT_PRIMARY, "done": TEXT_SECONDARY, "pending": TEXT_DIM}.get(status, TEXT_DIM)

        draw.text((50 + indent * 18, sub_y), sym, fill=sc, font=sub_font)
        draw_text_ellipsis(draw, title, 80 + indent * 18, sub_y, sub_font, tc, max_w=850 - indent * 18)
        sub_y += 34

    # === TOOL CALL HISTORY ===
    tc_y = sub_y + 20
    draw.text((40, tc_y), "TOOL CALLS", fill=TEXT_DIM, font=get_font(14, bold=True))
    tc_y += 28

    tc_font = get_font(17, mono=True)
    for i, tc in enumerate(tool_calls):
        show = progress > (i + 1) / (len(tool_calls) + 1)
        if not show:
            break

        ttype = tc.get("type", "")
        tstatus = tc.get("status", "")
        telapsed = tc.get("elapsed", "")
        color_map = {"Bash": TEXT_BLUE, "Read": TEXT_GREEN, "Write": TEXT_YELLOW, "Edit": TEXT_PURPLE}
        tcolor = color_map.get(ttype, TEXT_PRIMARY)

        draw.text((55, tc_y), "%s(...)" % ttype, fill=tcolor, font=tc_font)
        status_color = TEXT_GREEN if tstatus == "completed" else TEXT_YELLOW
        draw.text((250, tc_y), "→ Status: %s" % tstatus, fill=status_color, font=tc_font)
        draw.text((550, tc_y), "%s" % telapsed, fill=TEXT_DIM, font=tc_font)
        tc_y += 32

    # === STATUS BAR ===
    draw.rectangle([(40, H - 60), (W - 40, H - 10)], fill=BG_INPUT, outline=BORDER_COLOR, width=1)
    draw.text((55, H - 45), "● main", fill=TEXT_GREEN, font=get_font(14))
    ctx = int(80 + 15 * progress)
    draw.text((200, H - 45), "%d%% context used" % ctx, fill=TEXT_DIM, font=get_font(14))

    # Agent count
    done_count = sum(1 for a in agents if a.get("progress_end", 80) * progress >= 100)
    total_ag = len(agents)
    draw.text((W - 300, H - 45), "%d/%d agents working" % (total_ag - done_count, total_ag), fill=TEXT_DIM, font=get_font(14))

    return img


# ================================================================
# RENDER: bash_tool_call_state (line-by-line reveal)
# ================================================================
def render_bash_tool_call_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)

    output_lines = payload.get("output_lines", [])
    total_lines = len(output_lines)

    # === TOOL CARD HEADER ===
    draw.rounded_rectangle([(40, 30), (W - 40, 75)], radius=8, fill=(28, 28, 40), outline=(60, 60, 80), width=1)
    draw.text((60, 45), "Bash(", fill=TEXT_BLUE, font=get_font(20, bold=True, mono=True))
    draw.text((130, 45), payload.get("command", ""), fill=TEXT_PRIMARY, font=get_font(18, mono=True))
    draw.text((60, 105), ")", fill=TEXT_BLUE, font=get_font(20, bold=True, mono=True))

    # Thought
    thought = payload.get("thought", "")
    if thought and progress < 0.15:
        draw.text((60, 150), "/* %s */" % thought, fill=TEXT_DIM, font=get_font(16))

    # Result badge (if provided)
    result_badge = payload.get("result_badge", "")
    if result_badge and progress > 0.7:
        badge_color = TEXT_GREEN if "PASS" in result_badge else TEXT_RED
        draw.rounded_rectangle([(60, 100), (500, 135)], radius=8, fill=(20, 45, 25) if "PASS" in result_badge else (45, 20, 20), outline=badge_color, width=1)
        draw.text((280, 117), result_badge, fill=badge_color, font=get_font(20, bold=True), anchor="mm")

    # === OUTPUT LINES (progressive reveal) ===
    term_y = 170
    term_font = get_font(24, mono=True)

    visible_count = int(total_lines * progress) + 1
    if visible_count > total_lines:
        visible_count = total_lines

    for i in range(visible_count):
        line = output_lines[i]
        text = line.get("text", "")
        if not text:
            term_y += 10
            continue
        cn = line.get("color", "white")
        cm = {"white": TEXT_PRIMARY, "green": TEXT_GREEN, "red": TEXT_RED,
              "yellow": TEXT_YELLOW, "dim": TEXT_DIM, "blue": TEXT_BLUE}
        draw.text((60, term_y), text, fill=cm.get(cn, TEXT_PRIMARY), font=term_font)
        term_y += 44

    # Last line is the command, highlight it
    if visible_count >= 1:
        draw.text((60, 170), output_lines[0].get("text", ""), fill=TEXT_YELLOW, font=term_font)

    # === SUCCESS INDICATOR (pulse at end) ===
    if progress > 0.85:
        exit_code = payload.get("exit_code", 0)
        status = payload.get("status", "completed")
        elapsed = payload.get("elapsed", "")

        # Green success box
        success_y = max(term_y + 30, 600)
        draw.rounded_rectangle([(60, success_y), (W - 60, success_y + 70)], radius=12,
                               fill=(20, 45, 25), outline=TEXT_GREEN, width=2)

        if progress > 0.9:
            pulse_alpha = int(128 * (1 - abs(math.sin(progress * 30 * math.pi))))
            pulse_color = (80, 220, 120, pulse_alpha)
            # Can't do alpha in RGB, so just use solid
            draw.text((W // 2, success_y + 35), "✓  Exit code: %d  %s" % (exit_code, status),
                      fill=TEXT_GREEN, font=get_font(26, bold=True), anchor="mm")
        else:
            draw.text((W // 2, success_y + 35), "✓  Exit code: %d  %s" % (exit_code, status),
                      fill=TEXT_GREEN, font=get_font(26, bold=True), anchor="mm")

        # Elapsed
        draw.text((W // 2, success_y + 105), "Completed in %s" % elapsed,
                  fill=TEXT_DIM, font=get_font(18), anchor="mm")

    return img


# ================================================================
# RENDER: write_tool_call_state (code lines type in)
# ================================================================
def render_write_tool_call_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)
    frame_idx = int(progress * 3.5 * FPS)

    code_lines = payload.get("code_lines", [])
    total_lines = len(code_lines)
    file_path = payload.get("file_path", "")
    total_line_count = payload.get("total_lines", len(code_lines))
    status = payload.get("status", "created")
    key_lines = payload.get("key_lines", [])

    # === TOOL CARD HEADER ===
    draw.rounded_rectangle([(40, 30), (W - 40, 75)], radius=8, fill=(28, 28, 40), outline=(60, 60, 80), width=1)
    draw.text((60, 45), "Write(", fill=TEXT_YELLOW, font=get_font(20, bold=True, mono=True))
    draw.text((150, 45), file_path, fill=TEXT_BLUE, font=get_font(18, mono=True))

    # Line counter — increments as lines appear
    visible_lines = min(total_lines, int(total_lines * progress) + 1)
    draw.text((60, 100), "%d/%d lines written" % (visible_lines, total_line_count),
              fill=TEXT_DIM, font=get_font(16))

    # === CODE WINDOW (V5: bigger font 26, key line highlight) ===
    code_w, code_h = 1000, 580
    code_x, code_y = 40, 140
    draw.rounded_rectangle([code_x, code_y, code_x + code_w, code_y + code_h],
                           radius=10, fill=(22, 22, 28), outline=BORDER_COLOR, width=2)

    # Line number gutter
    gutter_x = code_x + 25
    gutter_font = get_font(18, mono=True)

    code_font = get_font(26, mono=True)
    line_y = code_y + 18
    line_height = 38

    for i in range(visible_lines):
        # Highlight key lines
        if i in key_lines:
            draw.rectangle([code_x + 10, line_y - 2, code_x + code_w - 10, line_y + line_height],
                          fill=(30, 45, 35))
        # Line number
        draw.text((gutter_x, line_y), "%2d" % (i + 1), fill=TEXT_DIM, font=gutter_font)
        # Code
        draw.text((gutter_x + 45, line_y), code_lines[i], fill=TEXT_PRIMARY, font=code_font)
        line_y += line_height

    # Cursor at next line position
    if visible_lines < total_lines and frame_idx % 20 < 10:
        draw.rectangle([(gutter_x + 45, line_y), (gutter_x + 45 + 14, line_y + 28)], fill=CURSOR_COLOR)

    # === STATUS BADGE ===
    sc_map = {"created": TEXT_GREEN, "overwritten": TEXT_YELLOW, "error": TEXT_RED}
    sc = sc_map.get(status, TEXT_GREEN)
    if progress > 0.8:
        draw.rounded_rectangle([(40, code_y + code_h + 20), (40 + 150, code_y + code_h + 60)], radius=8,
                               fill=(20, 45, 25), outline=sc, width=2)
        draw.text((115, code_y + code_h + 40), status.upper(), fill=sc,
                  font=get_font(18, bold=True), anchor="mm")
        draw.text((220, code_y + code_h + 40), "File saved successfully",
                  fill=TEXT_DIM, font=get_font(18))

    return img


# ================================================================
# RENDER: edit_diff_tool_state (diff lines slide in)
# ================================================================
def render_edit_diff_tool_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)

    file_path = payload.get("file_path", "")
    added = payload.get("diff_lines_added", 0)
    removed = payload.get("diff_lines_removed", 0)
    preview = payload.get("diff_preview", [])
    thought = payload.get("thought", "")
    fix_header = payload.get("fix_header", "")
    key_lines_add = payload.get("key_lines_add", [])

    # === TOOL CARD HEADER ===
    draw.rounded_rectangle([(40, 30), (W - 40, 75)], radius=8, fill=(28, 28, 40), outline=(60, 60, 80), width=1)
    draw.text((60, 45), "Edit(", fill=TEXT_PURPLE, font=get_font(20, bold=True, mono=True))
    draw.text((140, 45), file_path, fill=TEXT_BLUE, font=get_font(18, mono=True))

    # Fix header badge (v5)
    if fix_header:
        draw.rounded_rectangle([(60, 95), (600, 130)], radius=8, fill=(30, 40, 30), outline=TEXT_GREEN, width=1)
        draw.text((330, 112), fix_header, fill=TEXT_GREEN, font=get_font(18, bold=True), anchor="mm")

    # Diff counter — animated
    current_added = int(added * progress)
    current_removed = int(removed * progress)
    # Diff counter — animated
    counter_y = 140 if fix_header else 100
    draw.text((60, counter_y), "+%d  -%d  lines changed" % (current_added, current_removed),
              fill=TEXT_GREEN, font=get_font(18))

    # === DIFF WINDOW (V5: bigger font 24, key line highlight) ===
    diff_w, diff_h = 1000, 660
    diff_x = 40
    diff_y = 170 if fix_header else 140
    draw.rounded_rectangle([diff_x, diff_y, diff_x + diff_w, diff_y + diff_h],
                           radius=10, fill=(20, 20, 26), outline=BORDER_COLOR, width=2)

    diff_font = get_font(24, mono=True)
    line_y = diff_y + 15
    line_h = 36
    total_diff = len(preview)
    visible_diff = int(total_diff * progress) + 1

    for i in range(min(visible_diff, total_diff)):
        entry = preview[i]
        dtype = entry.get("type", "context")
        text = entry.get("text", "")
        prefix = {"add": "+", "remove": "-", "context": " "}[dtype]
        color = {"add": TEXT_GREEN, "remove": TEXT_RED, "context": TEXT_DIM}[dtype]
        bg = {"add": (20, 40, 25), "remove": (40, 20, 20), "context": None}[dtype]

        # Slide-in effect for add/remove lines
        if dtype in ("add", "remove"):
            slide = min(1.0, (progress * total_diff - i) * 3)
            if slide < 0:
                line_y += line_h
                continue
        else:
            slide = 1.0 if progress > i / total_diff else 0

        if slide <= 0:
            line_y += line_h
            continue

        # Key line highlight for fix scenes
        is_key = (dtype == "add" and i in key_lines_add)
        if is_key:
            draw.rectangle([diff_x + 10, line_y - 2, diff_x + diff_w - 10, line_y + line_h],
                          fill=(25, 50, 30))

        if bg and not is_key:
            draw.rectangle([diff_x + 10, line_y - 2, diff_x + diff_w - 10, line_y + line_h - 2], fill=bg)
        draw.text((diff_x + 25, line_y), "%s %s" % (prefix, text), fill=color, font=diff_font)
        line_y += line_h

    return img


# ================================================================
# RENDER: error_tool_call_state (red flash + error reveal)
# ================================================================
def render_error_tool_call_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)

    output_lines = payload.get("output_lines", [])
    total_lines = len(output_lines)
    error_moment = payload.get("error_moment", 0.65)
    exit_code = payload.get("exit_code", 1)

    # === TOP RED WARNING BAND (pulses at error moment) ===
    is_flash = abs(progress - error_moment) < 0.05
    if is_flash:
        # Flash red for ~3 frames
        draw.rectangle([(0, 0), (1080, 1920)], fill=(40, 10, 10))
    else:
        # Subtle red glow at top
        glow_intensity = 20 if progress > error_moment else 0
        draw.rectangle([(0, 0), (1080, glow_intensity)], fill=(40, 10, 10))

    # Error title badge (V5)
    error_title = payload.get("error_title", "")
    if error_title and progress > 0.3:
        draw.rounded_rectangle([(60, 95), (550, 135)], radius=8, fill=(50, 15, 15), outline=TEXT_RED, width=2)
        draw.text((305, 115), error_title, fill=TEXT_RED, font=get_font(22, bold=True), anchor="mm")

    # === TOOL CARD HEADER ===
    title_y = 150 if error_title else 30
    draw.rounded_rectangle([(40, title_y), (W - 40, title_y + 45)], radius=8, fill=(35, 25, 25), outline=(80, 40, 40), width=1)
    draw.text((60, title_y + 15), "Bash(", fill=TEXT_RED, font=get_font(20, bold=True, mono=True))
    draw.text((130, title_y + 15), payload.get("command", ""), fill=TEXT_PRIMARY, font=get_font(18, mono=True))

    # === OUTPUT LINES (progressive reveal) ===
    term_font = get_font(22, mono=True)
    line_y = title_y + 75

    visible_count = int(total_lines * progress) + 1
    if visible_count > total_lines:
        visible_count = total_lines

    for i in range(visible_count):
        line = output_lines[i]
        text = line.get("text", "")
        if not text:
            line_y += 10
            continue
        cn = line.get("color", "white")
        cm = {"white": TEXT_PRIMARY, "green": TEXT_GREEN, "red": TEXT_RED,
              "yellow": TEXT_YELLOW, "dim": TEXT_DIM}
        draw.text((60, line_y), text, fill=cm.get(cn, TEXT_PRIMARY), font=term_font)
        line_y += 38

    # === ERROR ICON (pulses after error moment) ===
    if progress > error_moment:
        pulse = abs(math.sin(progress * 20))
        size = int(60 + 20 * pulse)
        draw.text((540, 1200), "⚠", fill=TEXT_RED, font=get_font(size, bold=True), anchor="mm")
        draw.text((540, 1300), "FAILED (exit code: %d)" % exit_code,
                  fill=TEXT_RED, font=get_font(28, bold=True), anchor="mm")
        draw.text((540, 1360), "%d candidates failed validation" % payload.get("error_count", 17),
                  fill=TEXT_DIM, font=get_font(18), anchor="mm")

    # Red left accent
    draw.rectangle([(0, 0), (6, H)], fill=TEXT_RED)

    return img


# ================================================================
# RENDER: agent_complete_state
# ================================================================
def render_agent_complete_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)
    frame_idx = int(progress * 4.0 * FPS)

    agents = payload.get("agents", [])
    summary = payload.get("summary", "")
    file_count = payload.get("file_count", 0)

    # === HEADER ===
    draw.text((540, 40), "MULTI-AGENT PIPELINE", fill=TEXT_DIM, font=get_font(18, bold=True), anchor="mm")
    draw.text((540, 75), "All agents completed", fill=TEXT_GREEN, font=get_font(28, bold=True), anchor="mm")

    check_y = 130
    check_font = get_font(20)
    complete_interval = 4.0 / len(agents)

    for i, agent in enumerate(agents):
        # Each agent completes at staggered progress points
        agent_progress = min(1.0, max(0, (progress - i * 0.05) / 0.6))

        if agent_progress <= 0:
            check_y += 65
            continue

        name = agent.get("name", "")
        role = agent.get("role", "worker")
        tokens_val = agent.get("tokens", 10000)
        elapsed_val = agent.get("elapsed_seconds", 30)

        # Agent row
        draw.rounded_rectangle([(40, check_y), (W - 40, check_y + 58)], radius=8,
                               fill=(22, 28, 24), outline=(40, 60, 44), width=1)

        # Green checkmark
        draw.text((60, check_y + 12), "✓", fill=TEXT_GREEN, font=get_font(24, bold=True))
        draw.text((96, check_y + 14), name, fill=TEXT_PRIMARY, font=get_font(20, bold=True))
        draw.text((260, check_y + 14), role.upper(), fill=TEXT_DIM, font=get_font(14, bold=True))

        # Stats
        stats_str = "%s · ↓ %s tokens" % (format_time(elapsed_val), format_tokens(tokens_val))
        draw.text((96, check_y + 36), stats_str, fill=TEXT_DIM, font=get_font(15))

        # Completed badge
        draw.rounded_rectangle([(W - 220, check_y + 12), (W - 60, check_y + 44)], radius=6,
                               fill=(20, 50, 25), outline=TEXT_GREEN, width=1)
        draw.text((W - 140, check_y + 28), "DONE", fill=TEXT_GREEN, font=get_font(16, bold=True), anchor="mm")

        check_y += 70

    # === SUMMARY (revealed after all agents) ===
    if progress > 0.8:
        sum_y = max(check_y + 30, 700)

        draw.rounded_rectangle([(40, sum_y), (W - 40, sum_y + 90)], radius=12,
                               fill=(22, 28, 36), outline=(50, 60, 70), width=2)

        draw.text((540, sum_y + 25), summary, fill=TEXT_GREEN, font=get_font(22, bold=True), anchor="mm")
        draw.text((540, sum_y + 60), "%d files created" % file_count, fill=TEXT_DIM, font=get_font(18), anchor="mm")

    # === STATUS BAR ===
    draw.rectangle([(40, H - 60), (W - 40, H - 10)], fill=BG_INPUT, outline=BORDER_COLOR, width=1)
    draw.text((55, H - 45), "● main", fill=TEXT_GREEN, font=get_font(14))
    draw.text((W - 300, H - 45), "All agents completed", fill=TEXT_GREEN, font=get_font(14, bold=True))

    # Animated sparkle at end
    if progress > 0.9:
        sparkle = "*" * int(abs(math.sin(frame_idx * 0.1)) * 10)
        draw.text((540, sum_y - 40), sparkle, fill=TEXT_YELLOW, font=get_font(14), anchor="mm")

    return img


# ================================================================
# RENDER: final_status_v4_state
# ================================================================
def render_final_status_v4_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)

    verdict = payload.get("verdict", "PASS")
    summary = payload.get("summary", "")
    stats = payload.get("stats", [])
    badges = payload.get("badges", [])

    vc_map = {"PASS": TEXT_GREEN, "FAIL": TEXT_RED, "PASS_WITH_ERRORS": TEXT_YELLOW}
    vc = vc_map.get(verdict, TEXT_GREEN)

    # === VERDICT (scale-up animation) ===
    if progress < 0.15:
        # Scale up from small
        scale = progress / 0.15
        verdict_size = int(32 + 32 * scale)
    else:
        # Pulsing at full size
        pulse = 1 + 0.05 * math.sin(progress * 12 * math.pi)
        verdict_size = int(64 * pulse)

    draw.text((540, 120), verdict, fill=vc, font=get_font(verdict_size, bold=True), anchor="mm")

    # Summary
    if progress > 0.1:
        draw.text((540, 200), summary, fill=TEXT_PRIMARY, font=get_font(26), anchor="mm")

    # === STATS CARDS (slide up) ===
    card_w, card_h = 220, 130
    total_cards = len(stats)
    spacing = 25
    start_x = (W - (card_w * total_cards + spacing * (total_cards - 1))) // 2

    for i, stat in enumerate(stats):
        show_at = 0.2 + i * 0.18
        if progress < show_at:
            continue

        card_progress = min(1.0, (progress - show_at) / 0.15)
        # Slide up from y+50
        cy_offset = int(50 * (1 - card_progress))
        cx = start_x + i * (card_w + spacing)
        cy = 320 + cy_offset

        draw.rounded_rectangle([cx, cy, cx + card_w, cy + card_h], radius=14,
                               fill=(24, 26, 34), outline=BORDER_COLOR, width=2)

        # Icon
        icon_font = get_font(28)
        draw.text((cx + card_w // 2, cy + 25), stat.get("icon", ""), fill=TEXT_DIM, font=icon_font, anchor="mm")

        # Value
        value = stat.get("value", "")
        draw.text((cx + card_w // 2, cy + 60), value, fill=TEXT_PRIMARY, font=get_font(26, bold=True), anchor="mm")

        # Label
        label = stat.get("label", "")
        draw.text((cx + card_w // 2, cy + 100), label, fill=TEXT_DIM, font=get_font(16), anchor="mm")

    # === BADGES ===
    badge_y = 540
    for i, badge in enumerate(badges):
        show_at = 0.5 + i * 0.20
        if progress < show_at:
            continue

        btext = badge.get("text", "")
        bcolor = badge.get("color", "green")
        cm = {"green": TEXT_GREEN, "orange": TEXT_ORANGE, "blue": TEXT_BLUE, "yellow": TEXT_YELLOW}
        bc = cm.get(bcolor, TEXT_GREEN)

        badge_x = 540 + (i - 1) * 330

        draw.rounded_rectangle([badge_x - 145, badge_y, badge_x + 145, badge_y + 44], radius=8,
                               fill=(25, 30, 25) if bcolor == "green" else (30, 28, 22),
                               outline=bc, width=1)
        draw.text((badge_x, badge_y + 22), btext, fill=bc, font=get_font(16, bold=True), anchor="mm")

    # === DECORATIVE BOTTOM BAR ===
    draw.rectangle([(40, H - 70), (W - 40, H - 20)], fill=BG_INPUT, outline=BORDER_COLOR, width=1)
    draw.text((55, H - 55), "Pipeline: COMPLETE", fill=TEXT_GREEN, font=get_font(16, bold=True))



    return img


# ================================================================
# RENDER: hook_montage_state (V5 — fast-paced opening hook)
# Rapid flash of agent cards, tool calls, status to grab attention
# ================================================================
def render_hook_montage_state(payload, progress):
    img = create_base_image()
    draw = ImageDraw.Draw(img)
    total_frames = int(2.5 * FPS)
    frame_idx = int(progress * total_frames)

    flashes = payload.get("flashes", [])
    status_line = payload.get("status_line", "")

    # Background — dark terminal with subtle gradient
    draw.rectangle([(0, 0), (W, H)], fill=(14, 14, 18))

    # Determine which flash element to show based on progress
    total_flashes = len(flashes)
    current_idx = min(total_flashes - 1, int(progress * total_flashes * 1.5))

    # Show flash elements from current_idx going back a bit
    for idx in range(max(0, current_idx - 2), min(total_flashes, current_idx + 1)):
        flash = flashes[idx]
        delay = flash.get("delay", idx * 0.3)
        icon = flash.get("icon", "")
        text = flash.get("text", "")
        color = flash.get("color", "white")

        # Only show if we're past this flash's delay
        if progress < delay / 2.5:
            continue

        cmap = {
            "orange": TEXT_ORANGE, "white": TEXT_PRIMARY,
            "secondary": TEXT_SECONDARY, "blue": TEXT_BLUE,
            "yellow": TEXT_YELLOW, "purple": TEXT_PURPLE,
            "green": TEXT_GREEN, "red": TEXT_RED, "dim": TEXT_DIM
        }
        fc = cmap.get(color, TEXT_PRIMARY)

        # Tool call cards use monospace style, agents use different layout
        is_tool = text.startswith("Bash") or text.startswith("Write") or text.startswith("Edit") or text.startswith("Agent")

        # Position: flash elements spread vertically
        fy = 300 + (idx * 120)
        if fy > 1600:
            fy = 300 + ((idx % 5) * 140)

        if is_tool:
            # Tool card style
            draw.rounded_rectangle([(100, fy), (W - 100, fy + 70)], radius=8,
                                   fill=(24, 26, 34), outline=(50, 50, 60), width=1)
            draw.text((130, fy + 18), text, fill=fc, font=get_font(24, bold=True, mono=True))
            # Fade out effect for older items
            if idx < current_idx - 1:
                draw.rounded_rectangle([(100, fy), (W - 100, fy + 70)], radius=8,
                                       fill=(14, 14, 18), outline=(30, 30, 36), width=1)
                draw.text((130, fy + 18), text, fill=TEXT_DIM, font=get_font(24, bold=True, mono=True))
        else:
            if icon == "■":
                draw.rectangle([(100, fy - 5), (105, fy + 25)], fill=TEXT_ORANGE)
            elif icon == "●":
                draw.ellipse([(100, fy), (115, fy + 15)], fill=TEXT_GREEN)
            elif icon == "◯":
                draw.ellipse([(100, fy), (115, fy + 15)], outline=TEXT_DIM, width=2)
            draw.text((135, fy - 2), text, fill=fc, font=get_font(26, bold=("main" in text or "Kimi" in text)))

    # === Status line at top ===
    if progress > 0.1:
        draw.text((540, 60), status_line, fill=TEXT_DIM, font=get_font(16), anchor="mm")

    # === Animated "loading" dots ===
    dots = "." * (frame_idx // 8 % 4)
    if progress < 0.9:
        draw.text((540, 1800), "Initializing pipeline%s" % dots, fill=TEXT_DIM, font=get_font(16), anchor="mm")

    # === Progress bar at bottom — fills across the hook ===
    bar_x, bar_y = 140, 1840
    bar_w, bar_h = 800, 6
    bar_progress = min(100, progress * 120)
    draw.rounded_rectangle([bar_x, bar_y, bar_x + bar_w, bar_y + bar_h], radius=3, fill=(40, 40, 50))
    if bar_progress > 0:
        draw.rounded_rectangle([bar_x, bar_y, bar_x + int(bar_w * bar_progress / 100), bar_y + bar_h],
                               radius=3, fill=TEXT_GREEN)

    return img


# ================================================================
# MAIN — multi-frame render loop
# ================================================================
RENDER_FUNCTIONS = {
    "hook_montage_state": render_hook_montage_state,
    "idle_terminal_state": render_idle_terminal_state,
    "context_compression_state": render_context_compression_state,
    "multi_agent_launch_state": render_multi_agent_launch_state,
    "multi_agent_running_state": render_multi_agent_running_state,
    "bash_tool_call_state": render_bash_tool_call_state,
    "write_tool_call_state": render_write_tool_call_state,
    "edit_diff_tool_state": render_edit_diff_tool_state,
    "error_tool_call_state": render_error_tool_call_state,
    "agent_complete_state": render_agent_complete_state,
    "final_status_v4_state": render_final_status_v4_state,
}

# Helper constant
HOLLOW_CIRCLE = (100, 104, 114)


def main():
    import time
    print("=" * 60)
    print("Claude Code UI Frame Renderer — v4 ULTRA")
    print("Multi-frame per-scene rendering engine")
    print("=" * 60)

    with open(CONFIG_PATH, "r", encoding="utf-8") as f:
        data = json.load(f)

    scenes = data.get("scenes", [])
    print("Loaded %d scenes" % len(scenes))

    total_frames = 0
    start_time = time.time()

    for scene in scenes:
        sid = scene["scene_id"]
        st = scene["state_type"]
        payload = scene.get("payload", {})
        dur = scene.get("duration_seconds", 3)
        num_frames = int(dur * FPS)

        fn = RENDER_FUNCTIONS.get(st)
        if not fn:
            print("  ⚠ No renderer for %s" % st)
            continue

        print("\n--- %s (%s) — %d frames ---" % (sid, st, num_frames))
        scene_start = time.time()

        for f_idx in range(num_frames):
            progress = f_idx / num_frames
            img = fn(payload, progress)
            out_path = os.path.join(FRAMES_DIR, "%s_%04d.png" % (sid, f_idx))
            img.save(out_path, "PNG")

            if f_idx % 30 == 0 or f_idx == num_frames - 1:
                elapsed = time.time() - scene_start
                pct = (f_idx + 1) / num_frames * 100
                print("  [%3d%%] frame %d/%d (%.1fs)" % (int(pct), f_idx + 1, num_frames, elapsed))

        total_frames += num_frames
        scene_elapsed = time.time() - scene_start
        print("  [OK] %d frames in %.1fs" % (num_frames, scene_elapsed))

    total_elapsed = time.time() - start_time
    print("\n" + "=" * 60)
    print("RENDER COMPLETE: %d total frames in %.1fs" % (total_frames, total_elapsed))
    print("Output: %s" % FRAMES_DIR)
    print("=" * 60)


if __name__ == "__main__":
    main()
